iOS 事件处理(2)
接上面
具体例子
用自定义触摸事件来实现相关 UIGestureRecognizer。这一小节的详细代码可见Multitouch EventsListing 3-1
至Listing 3-7
。
处理点击手势
用 UITouch 的 tapCount 属性来判断是单击还是双击还是三击。最好是在 touchesEnded:withEvent: 方法里面做判断处理,因为它是用户手指离开 App 时才响应的,要确保它真的是个 tap 手势,而不是拖动啥的。
|
|
处理滑动和拖动手势
滑动手势
从三个角度判断它是否是一个滑动手势
- Did the user’s finger move far enough?
- Did the finger move in a relatively straight line?
- Did the finger move quickly enough to call it a swipe?
具体代码如下:
|
|
拖动手势
简单的一根手指拖动的相关代码。
|
|
多点触摸
taps,drags,swipes 通常都只涉及了一个 touch,比较简单去跟踪。但是处理由多个 touches 组成的触摸事件时,比较有挑战性。需要去记录 touch 的所有相关属性,并且改变它的 state 等等。需要做到两点:
- Set the view’s multipleTouchEnabled property to YES;将多点触摸属性置为 YES;
- Use a Core Foundation dictionary object (CFDictionaryRef) to track the mutations of touches through their phases during the event;用 CFDictionaryRef 来跟着 UITouch,这里用 CFDictionaryRef 而不是 NSDictionary,因为 NSDictionary 会 copy 它的 key。而 UITouch 没有采取 NSCopying 协议。
Determining when the last touch in a multitouch sequence has ended,判断 multitouch sequence 里的最后一个 touch 是否结束,可以用下面的代码
|
|
指定自定义的触摸事件行为
通过改变一些属性去改变事件流的处理。
多个 touch 的分发传递(multipleTouchEnabled)
Turn on delivery of multiple touches. 默认值为NO,意味着只会接受触摸队列里面的第一个 touch,其他的会忽略掉。所以,[touches anyObject]
方法就只会返回一个对象。将属性 multipleTouchEnabled 设为 YES,则可以处理多个 touches。
限制事件只分发给一个 view(exclusiveTouch)
Restrict event delivery to a single view. 即只有一个 view 响应事件。默认情况下,view 的 exclusiveTouch 属性为 NO,这就意味着,一个 view 不会阻塞 window里的其他 view 去接受事件。如果将某个 view 的 exclusiveTouch 设为 YES,那么当它接受 touches 时,只会有它一个接收 touches 。这里举例说明了 exclusiveTouch 属性,A、B、C 3个 view 多点触摸的例子。
If the user touches inside A, it recognizes the touch. But if a user holds one finger inside view B and also touches inside view A, then view A does not receive the touch because it was not the only view tracking touches. Similarly, if a user holds one finger inside view A and also touches inside view B, then view B does not receive the touch because view A is the only view tracking touches. At any time, the user can still touch both B and C, and those views can track their touches simultaneously.
exclusiveTouch 这个属性比较傲娇,只有当设置它的为 YES 的 view 首先收到触摸事件时,它才能响应。
限制事件分发到 subviews 上 (hitTest:withEvent:)
Restrict event delivery to subviews. 重载 hitTest:withEvent: 方法返回自己 self。
关闭事件的分发
- userInteractionEnabled 属性置为 NO;
- hidden 属性置为 NO;
- alpha 属性值 <= 0.01;
阶段性的关闭事件的分发
beginIgnoringInteractionEvents 方法关闭,endIgnoringInteractionEvents 方法开启。这个方法是 UIApplication 的,所以能做一些全局性的事情。
触摸事件的转发
你可以将一个事件转发给另外一个响应对象(响应链就是这样玩的嘛),当你使用这个技术的时候得小心,因为 UIKit 没有设计去接受不属于它们的事件。所以,你不要转发给 UIKit 框架的对象。如果你想要有条件的去转发事件给其他响应对象时,那么这些对象应该是 UIView 的实例,并且这些对象关心事件的转发,并且能够处理这些事件。原因如下:
For a responder object to handle a touch, the touch’s view property must hold a reference to the responder.
一个 responder 对象想要处理一个 touch ,那么 touch 的 view 属性必须持有这个 responder。
事件的转发经常需要去分析 touch 对象觉得它是否应该转发事件。这里有一些方法你可以采取去分析:
- With an “overlay” view, such as a common superview, use hit-testing to intercept events for analysis prior to forwarding them to subviews.(使用 overlay view,例如公用的父视图,在转发到 subviews 之前拦截事件去分析)
- Override sendEvent: in a custom subclass of UIWindow, analyze touches, and forward them to the appropriate responders.(UIWindow 的子类重载 sendEvent: 方法,将事件转发到合适的 responders)
重载 sendEvent: 方法可以监听 App 事件的接收。UIApplication 和 UIWindow 都是用这个方法来分发事件的,所以它就是事件进入 App 的管道一样。当你重载的时候,务必调用父类的实现,[super sendEvent:event]。在 control 和 gesture recognizer 的响应事件里面打断点,可以看到,事件走的 UIKit 开始传递都是先走的,[UIApplication sendEvent:]、[UIWindow sendEvent:],最终都是走的 [UIApplication sendAction:to:from:forEvent:]。
处理多点触摸事件的最佳实践
当处理 touch 和 motion 事件时,这里有一些值得推荐的技巧和模式:
- 记得实现事件的取消方法。
- 如果自定义的是 UIView、UIViewController、UIResponder的子类时,你应该实现所有的事件方法,即使里面没有做任何实现。但是不要在里面调用父类的实现。
- 如果是其他 UIKit 的子类时,你没有必须实现所有的事件方法。但是,你必须得调用父类的实现。即 [super touchesBegan:touches withEvent:event] 。
- 只转发事件给 UIView 的子类。并确保这些转发后的对象能够知道并且处理这些不属于它的事件。
- 不要显示的通过 nextResponder 方法在响应链上发送事件。相反的,调用父类的实现,并且让 UIKit 去遍历处理。
- 不要使用 round-to-integer 代码(即不要使用 integer 来处理 float),这样会丢失精度。
- 如果在事件处理的时候需要创建持久对象,记得在 touchesCancelled:withEvent 和 touchesEnded:withEvent: 里面销毁它们。
- 如果你阻止它接受某个状态的 touch 事件时,可能导致结果不确定。不过,我们在实际中应该不会这样做。
需求
事件传递的最终目标是找到一个能够处理响应这个事件的对象(UIResponder 的子类)。如果找不到就丢弃它。
前提条件
能够处理事件的对象需要完成下面3个条件:
- 实现这四个方法
|
|
UIKit 已有的轮子
换汤不要药,跟前面的前提条件一样,只不过是另外一种形式来完成而已。
gesture recognizer
通过实现跟 UIResponder 相同签名的方法来完成。参考例子,上面有提到,官方文档 Listing 1-8 Implementation of a checkmark gesture recognizer 和 YYGestureRecognizer。
|
|
UIControl
也是内部实现跟 UIResponder 相同功能的方法来完成,里面通过一个 _targetActions 数组来存储各种 UIControlEvents 状态的事件。可以参考Chameleon UIControl 和 SVSegmentedControl。
|
|
过程
手指触摸屏幕就会生成 UIEvent 对象,然后放在 application 的队列里面,application 会从系统队列的顶层取出一个事件并分发它。application(sendEvent:) -> window(sendEvent:) -> initial object(hit-test view or frist responder)。
而 application 和 window 则是通过 Hit-Testing 和响应链来找到 initial object。一般情况下,都不需要我们去干涉 UIKit 的这个分发过程。但是,我们可以在这个过程去干涉达到自己的需求。
用途
这个章节的相关代码参考自
扩大触摸区域
我们绘制 UIButton 的时候,想要扩大它的响应区域。我们可以在 UIButton 里面处理 Hit-Testing 的那两个方法其中一个里面做处理。
hitTest:withEvent:
|
|
pointInside:withEvent:
|
|
superview 响应 subview 的事件
这个在限制事件分发到 subviews 上
小节里面就有说过。重载 hitTest:withEvent: 方法返回自己 self。
- Apple UIApplication sendEvent:
- Apple UIApplication sendAction:to:from:forEvent:
- Apple UIWindow sendEvent:
- Apple UIResponder
- Apple UIControl
- Apple UIGesturerecognizer
- Apple TargetAction
- iOS-Runtime-Headers _targetActions数组
- Chameleon 自定义控件里面
- Chameleon hitTest:withEvent:
- Chameleon pointInside:withEvent:
- 南峰子 UIKit: UIControl
- 南峰子 UIKit: UIResponder
install_url
to use ShareThis. Please set it in _config.yml
.